iT邦幫忙

2025 iThome 鐵人賽

DAY 2
0
Software Development

Laravel Pest TDD 實戰:從零開始的測試驅動開發系列 第 2

Day 02 - 認識斷言(Assertions) 🚀

  • 分享至 

  • xImage
  •  

今天要做什麼?

昨天我們成功建立了測試環境並寫下第一個測試,今天要深入了解測試的核心 —「斷言(Assertions)」。

想像一下,你正在開發一個使用者註冊功能。產品經理說:「我們需要驗證使用者輸入的 email 格式、密碼強度、年齡範圍...」你心想:「這麼多驗證規則,怎麼確保每一個都正確運作?」

答案就是斷言!斷言是測試的核心,它告訴我們「期望」和「實際」結果是否相符。今天我們要學會使用各種斷言方法,讓測試更精準、更具表達力。

學習目標

今天結束後,你將學會:

  • 理解斷言的作用和重要性
  • 掌握 Pest 的各種斷言語法
  • 學會選擇適當的斷言方法
  • 理解斷言失敗時的調試技巧
  • 實踐常見的驗證場景測試

TDD 學習地圖

第一階段:打好基礎(Day 1-10)
├── Day 01 - 環境設置與第一個測試
├── Day 02 - 認識斷言(Assertions) ★ 今天在這裡
├── ...
└── (更多精彩內容待續)

什麼是斷言? 🧮

斷言就像品質檢驗員,負責檢查產品是否符合規格。在測試中,斷言:

  • 驗證結果:檢查函數回傳值是否正確
  • 表達期望:清楚說明我們期望的行為
  • 提供回饋:當測試失敗時給出明確的錯誤訊息

基本斷言語法 📝

昨天我們已經使用了最基本的 toBe 斷言:

建立 tests/Unit/Day02/BasicAssertionsTest.php

<?php

describe('basic assertions', function () {
    it('comparesStrictEquality', function () {
        $result = 2 + 3;
        expect($result)->toBe(5);
    });
    
    it('differencesBetweenToBeAndToEqual', function () {
        $obj1 = ['name' => 'Alice'];
        $obj2 = ['name' => 'Alice'];
        
        // toBe 檢查是否為同一個物件(參考相等)
        expect($obj1)->not->toBe($obj2);
        
        // toEqual 檢查內容是否相同(值相等)
        expect($obj1)->toEqual($obj2);
    });
});

常用斷言方法 🔧

更新 tests/Unit/Day02/BasicAssertionsTest.php

<?php

describe('common assertion methods', function () {
    // 相等性檢查
    it('strictEquality', function () {
        expect(5)->toBe(5);
        expect('hello')->toBe('hello');
    });

    it('deepComparison', function () {
        $user = ['name' => 'Bob', 'age' => 30];
        expect($user)->toEqual(['name' => 'Bob', 'age' => 30]);
    });

    // null 檢查
    it('checksNullValues', function () {
        $nullValue = null;
        $notNullValue = 'exists';
        
        expect($nullValue)->toBeNull();
        expect($notNullValue)->not->toBeNull();
    });

    // 真假值檢查
    it('booleanChecks', function () {
        expect(true)->toBeTrue();
        expect(false)->toBeFalse();
        expect(1 > 0)->toBeTrue();
        expect(1 < 0)->toBeFalse();
    });

    // 類型檢查
    it('typeChecks', function () {
        expect([])->toBeArray();
        expect('hello')->toBeString();
        expect(42)->toBeInt();
        expect(3.14)->toBeFloat();
    });
});

數值比較斷言

數值比較在驗證功能中經常用到:

建立 tests/Unit/Day02/NumberAssertionsTest.php

<?php

describe('numeric comparisons', function () {
    it('greaterThan', function () {
        expect(10)->toBeGreaterThan(5);
    });

    it('greaterThanOrEqual', function () {
        expect(10)->toBeGreaterThanOrEqual(10);
        expect(15)->toBeGreaterThanOrEqual(10);
    });

    it('lessThan', function () {
        expect(5)->toBeLessThan(10);
    });

    it('lessThanOrEqual', function () {
        expect(5)->toBeLessThanOrEqual(5);
        expect(3)->toBeLessThanOrEqual(5);
    });

    it('rangeCheck', function () {
        expect(5)->toBeBetween(1, 10);
        expect(10)->toBeBetween(1, 10);
    });
});

字串相關斷言

字串驗證在表單處理中非常重要:

建立 tests/Unit/Day02/StringAssertionsTest.php

<?php

describe('string assertions', function () {
    it('containsSubstring', function () {
        expect('Hello World')->toContain('World');
        expect('user@example.com')->toContain('@');
    });

    it('regularExpressionMatching', function () {
        expect('hello123')->toMatch('/^hello\d+$/');
        expect('test@email.com')->toMatch('/\w+@\w+\.\w+/');
    });

    it('checksLength', function () {
        expect('hello')->toHaveLength(5);
        expect('')->toHaveLength(0);
    });

    it('startsAndEndsWithChecks', function () {
        expect('Hello World')->toStartWith('Hello');
        expect('Hello World')->toEndWith('World');
    });
});

實作練習:驗證器函數 🎯

讓我們實作一個驗證器類別來練習各種斷言:

建立 tests/Unit/Day02/ValidatorTest.php

<?php

use App\Validator;

describe('validator function tests', function () {
    beforeEach(function () {
        $this->validator = new Validator();
    });

    describe('isValidEmail', function () {
        it('acceptsValidEmails', function () {
            expect($this->validator->isValidEmail('user@example.com'))->toBeTrue();
            expect($this->validator->isValidEmail('test.user@company.co.uk'))->toBeTrue();
        });

        it('rejectsInvalidEmails', function () {
            expect($this->validator->isValidEmail('invalid'))->toBeFalse();
            expect($this->validator->isValidEmail('@example.com'))->toBeFalse();
            expect($this->validator->isValidEmail('user@'))->toBeFalse();
        });
    });

    describe('isStrongPassword', function () {
        it('acceptsStrongPasswords', function () {
            expect($this->validator->isStrongPassword('MyP@ss123'))->toBeTrue();
            expect($this->validator->isStrongPassword('Str0ng!Pass'))->toBeTrue();
        });

        it('rejectsWeakPasswords', function () {
            expect($this->validator->isStrongPassword('weak'))->toBeFalse();
            expect($this->validator->isStrongPassword('onlylowercase'))->toBeFalse();
            expect($this->validator->isStrongPassword('ONLYUPPERCASE'))->toBeFalse();
            expect($this->validator->isStrongPassword('NoNumbers!'))->toBeFalse();
        });
    });

    describe('isValidAge', function () {
        it('acceptsValidAgeRange', function () {
            expect($this->validator->isValidAge(25))->toBeTrue();
            expect($this->validator->isValidAge(18))->toBeTrue();
            expect($this->validator->isValidAge(65))->toBeTrue();
        });

        it('rejectsInvalidAges', function () {
            expect($this->validator->isValidAge(17))->toBeFalse();
            expect($this->validator->isValidAge(121))->toBeFalse();
            expect($this->validator->isValidAge(-5))->toBeFalse();
        });
    });
});

實作 Validator 類別

建立 app/Validator.php

<?php

namespace App;

class Validator
{
    public function isValidEmail(string $email): bool
    {
        return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
    }

    public function isStrongPassword(string $password): bool
    {
        if (strlen($password) < 8) {
            return false;
        }
        
        $hasUpperCase = preg_match('/[A-Z]/', $password);
        $hasLowerCase = preg_match('/[a-z]/', $password);
        $hasNumbers = preg_match('/\d/', $password);
        $hasSpecialChar = preg_match('/[!@#$%^&*(),.?":{}|<>]/', $password);
        
        return $hasUpperCase && $hasLowerCase && $hasNumbers && $hasSpecialChar;
    }

    public function isValidAge(int $age): bool
    {
        return $age >= 18 && $age <= 120;
    }
}

執行測試

執行所有 Day 02 的測試:

./vendor/bin/pest tests/Unit/Day02

你應該會看到所有測試通過,並且能清楚了解每個斷言的作用。

今天學到什麼?

1. 斷言的核心概念

  • 斷言是測試的心臟,負責驗證期望結果
  • 好的斷言讓測試意圖清晰易懂
  • 斷言失敗時提供有價值的調試資訊

2. Pest 斷言方法

  • toBe(): 嚴格相等比較
  • toEqual(): 深度內容比較
  • toBeTrue() / toBeFalse(): 布林值檢查
  • toBeGreaterThan() / toBeLessThan(): 數值比較
  • toContain() / toMatch(): 字串檢查

3. 斷言選擇策略

  • 選擇最具表達力的斷言方法
  • 使用描述性的變數名稱
  • 複雜邏輯分步驟驗證

4. 實際應用場景

  • 表單驗證邏輯
  • 資料格式化功能
  • 業務規則檢查

總結

今天我們深入學習了斷言的使用,從基本的相等比較到複雜的字串匹配、數值比較。透過實作驗證器類別,我們練習了選擇合適的斷言方法、編寫清晰的測試、分步驟驗證複雜邏輯。

斷言是測試的基石,掌握好斷言的使用,你的測試就會更加準確和有說服力。

明天我們將進入 TDD 的核心 —「紅綠重構循環」! 💪


本文是「Laravel Pest TDD 實戰:從零開始的測試驅動開發」系列的第二篇文章。我們正在學習測試驅動開發的基礎,從環境設置到掌握核心概念,一步步建立扎實的測試能力。


上一篇
Day 01 - 環境設置與第一個測試 🚀
下一篇
Day 03 - TDD 紅綠重構循環
系列文
Laravel Pest TDD 實戰:從零開始的測試驅動開發9
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言